﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Threading.Tasks;
using System.ComponentModel;

namespace LevelEditor
{
    public class Terrain
    {
        /// <summary>
        /// Vertex buffer that makes up the terrain mesh
        /// </summary>
        public VertexTextureMap[] vertecies;                                                                                                                  

        /// <summary>
        /// The color used for the dark part of the fernel shader
        /// </summary>
        public Color fernelColor = Color.Black;

        /// <summary>
        /// Index buffer for the terrain mesh
        /// </summary>
        public int[] indicies;

        private int _resX = 1;
        /// <summary>
        /// The x-axis resolution (in pixels) the terrain reads from the height map
        /// </summary>
        public int ResX
        {
            get { return _resX; }

            set { _resX = value; }
        }

        private int _resY = 1;
        /// <summary>
        /// The y-axis resolution (in pixels) the terrain reads from the height map
        /// </summary>
        public int ResY
        {
            get { return _resY; }
            set { _resY = value; }
        }

        private HeightMap _heightMap;
        /// <summary>
        /// The height map for the terrain
        /// </summary>
        public HeightMap HeightMap
        {
            get { return _heightMap; }

            set
            {
                _heightMap = value;
            }
        }

        /// <summary>
        /// The with of the terrain
        /// </summary>
        public int Width
        {
            get { return HeightMap.MapWidth; }
        }

        /// <summary>
        /// The height (z-depth) of the terrain
        /// </summary>
        public int Height
        {
            get { return HeightMap.MapHeight; }
        }

        /// <summary>
        /// a value that indicates that the vertecies will be visible on screen
        /// </summary>
        public bool drawPoints;

        /// <summary>
        /// The different textures that are painted on the terrain
        /// </summary>
        public BindingList<TerrainMaterial> textures = new BindingList<TerrainMaterial>();

        /// <summary>
        /// if true, draws the terrain as a wireframe mesh
        /// </summary>
        public bool wireframe;

        /// <summary>
        /// Indicates that the terrain has a texture loaded
        /// </summary>
        public bool hasTexture;

        /// <summary>
        /// Screen quad for debugging heightmaps
        /// </summary>
        ScreenQuad dbg_scrQuad = new ScreenQuad();

        /// <summary>
        /// Aaaaaaaannd another screen quad for debuggin an shit...
        /// </summary>
        ScreenQuad dbg_scrQuad2 = new ScreenQuad();

        private int NumActiveTextures
        {
            get
            {
                int outval = 0;
                for (int i = 0; i < textures.Count; i++)
                    if (textures[i] != null)
                        outval++;

                return outval;
            }
        }

        float frand(float x)
        {
            return (float)(MainMenu.random.NextDouble()) * x;
        }
               
        public Terrain(HeightMap heightmap, GraphicsDevice device)
        {
            HeightMap = heightmap;

            vertecies = CreateVertecies(out indicies);
            UpdateVertecies();

            Texture2D tex = new Texture2D(MainMenu.instance.graphics.device, 1, 1);
            Color[] data = new Color[] { new Color(0.5f, 0.5f, 0.5f) };
            tex.SetData(data);
            textures.Add(new TerrainMaterial(tex, "Gray", new BindingList<TerrainRule>()));

            dbg_scrQuad.ScreenRect = new Rectangle(-1, -1, 1, 2);
            dbg_scrQuad2.ScreenRect = new Rectangle(-1, -1, 1, 2);
        }

        /// <summary>
        /// Update the vertecies in the terrain to match current settings
        /// </summary>
        public void UpdateVertecies()
        {
           this.vertecies = CreateVertecies(out indicies);

            // apply textureing to the vertecies
            for (int i = 0; i < vertecies.Length; i++)
            {
                VertexTextureMap vertex = vertecies[i];

                List<TerrainMaterial> affectedTextures = new List<TerrainMaterial>();
                List<int> affectedIndicies = new List<int>();

                for (int texture_index = 0; texture_index < textures.Count; texture_index++)
                {
                    // if there are no rules for this material, than it should texture this vertex regardless
                    if (textures[texture_index].rules.Count == 0)
                    {
                        affectedTextures.Add(textures[texture_index]);
                        affectedIndicies.Add(texture_index);
                    }
                    else
                    {
                        bool followsRule = true;
                        for (int rule_index = 0; rule_index < textures[texture_index].rules.Count; rule_index++)
                        {
                            TerrainRule rule = textures[texture_index].rules[rule_index];
                            if (!vertex.FollowsRule(rule))
                            {
                                if (!rule.exclusive)
                                {
                                    followsRule = false;
                                    continue;
                                }
                                else
                                {
                                    followsRule = false;
                                    break;
                                }
                            }
                            else
                                followsRule = true;
                        }
                        if (followsRule)
                        {
                            affectedTextures.Add(textures[texture_index]);
                            affectedIndicies.Add(texture_index);
                        }
                    }
                }
                

                for (int weightIndex = 0; weightIndex < vertex.terrainWeights.Length; weightIndex++)
                {
                    if (affectedIndicies.Contains(weightIndex))
                        vertex.terrainWeights[weightIndex] = 1.0f / (float)(affectedIndicies.Count);
                }

                vertecies[i] = vertex;
            }
        }

        /// <summary>
        /// Read the height map and interpret vertecies from height data
        /// </summary>
        /// <returns>Interpreted height data</returns>
        private VertexTextureMap[] CreateVertecies(out int[] indicies)
        {
            List<VertexTextureMap> vertecies = new List<VertexTextureMap>();
            for (int y = 0; y < Height; y += ResX)
            {
                for (int x = 0; x < Width; x += ResY)
                {
                    Vector2 uv = new Vector2(x, y );

                    vertecies.Add(new VertexTextureMap(new Vector3(uv.X, 0.0f, uv.Y), Vector3.Up, uv, new float[8]));
                }
            }

            // smooth y positions
            for (int i = 1; i < vertecies.Count - 1; i++)
            {
                float y = (vertecies[i - 1].Position.Y + vertecies[i].Position.Y + vertecies[i + 1].Position.Y) / 3.0f;
                VertexTextureMap v = vertecies[i];
                v.Position.Y = y;
                vertecies[i] = v;
            }

            int w = Width / ResX;
            int h = Height / ResY;

            List<int> out_indicies = new List<int>();
            for (int y = 0; y < h - 1; y++)
            {
                for (int x = 0; x < w - 1; x++)
                {
                    // first triangle
                    out_indicies.Add(x + (y + 1) * w);
                    out_indicies.Add(x + y * w);
                    out_indicies.Add((x + 1) + (y + 1) * w);

                    // second triangle
                    out_indicies.Add(x + y * w);
                    out_indicies.Add((x + 1) + (y + 1) * w);
                    out_indicies.Add((x + 1) + y * w);
                }
            }

            for (int i = 0; i < vertecies.Count; i++)
            {
                VertexTextureMap v = vertecies[i];
                v.Normal = Vector3.Up;
                v.terrainWeights[1] = 1.0f;
                vertecies[i] = v;
            }

            indicies = out_indicies.ToArray();

            return vertecies.ToArray();
        }

        private int sortVertecies(VertexPositionNormalTexture a, VertexPositionNormalTexture b)
        {
            if (a.Position.Length() > b.Position.Length())
                return 1;

            return -1;
        }

        /// <summary>
        /// Renders the terrain to the screen
        /// </summary>
        public void Draw() 
        {
            Camera.instance.FormatEffect(EffectManager.TerrainShader);
            Effect shader = EffectManager.TerrainShader;
            HeightMap.Update();
            shader.Parameters["lightDirection"].SetValue(Vector3.Down);
            shader.Parameters["lowest"].SetValue(HeightMap.lowest);
            shader.Parameters["highest"].SetValue(HeightMap.heighest);
            shader.Parameters["worldWidth"].SetValue(HeightMap.MapWidth);
            shader.Parameters["worldHeight"].SetValue(HeightMap.MapHeight);
            shader.Parameters["heightmap"].SetValue(HeightMap.vertexData);
            shader.Parameters["normalMap"].SetValue(HeightMap.normalMap);

            Graphics g = MainMenu.instance.graphics;
            //EffectManager.BasicShader.CurrentTechnique.Passes[0].Apply();
            //g.device.Textures[0] = HeightMap.data;
            //g.device.BlendState = BlendState.AlphaBlend;
            //dbg_scrQuad.Draw();

            //EffectManager.BasicShader.CurrentTechnique.Passes[0].Apply();
            //g.device.Textures[0] = HeightMap.normalMapDebug;
            //dbg_scrQuad.Draw();




            g.device.Textures[0] = HeightMap.data;
            g.device.SamplerStates[0] = SamplerState.PointClamp;
            for (int i = 2; i < textures.Count + 2; i++)
            {
                g.device.Textures[i] = textures[i - 2].texture;
            }

            shader.CurrentTechnique.Passes[0].Apply();
            g.device.DrawUserIndexedPrimitives<VertexTextureMap>(PrimitiveType.TriangleList, vertecies, 0, vertecies.Length, indicies, 0, indicies.Length / 3);
            g.effect = null;


#if FALSE
            Effect e = EffectManager.DepthQueue;
            Camera.instance.FormatEffect(e);
            e.CurrentTechnique.Passes[0].Apply();
            g.device.DrawUserIndexedPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, verts, 0, vertecies.Length, indicies, 0, indicies.Length / 3);
#endif

            if (drawPoints)
            {
                System.Drawing.Point cursorPosition = MainMenu.instance.viewWindow1.PointToClient(System.Windows.Forms.Cursor.Position);
                Ray cameraRay = Camera.instance.ComputeRay(MainMenu.instance.viewWindow1.viewport, cursorPosition.X, cursorPosition.Y);

                BoundingFrustum b = new BoundingFrustum(Camera.instance.View * Camera.instance.Projection);
                Matrix project = Camera.instance.World * Camera.instance.View * Matrix.CreateOrthographicOffCenter(0, MainMenu.instance.viewWindow1.Width, MainMenu.instance.viewWindow1.Height, 0, 0, 1);
                foreach (var v in vertecies)
                {
                    if (b.Contains(v.Position) != ContainmentType.Contains || Vector3.Distance(v.Position, Camera.instance.position) > 30)
                        continue;
                    BoundingSphere s = new BoundingSphere(v.Position, 1f);
                    bool picked = cameraRay.Intersects(s) != null;

                    Vector3 point = MainMenu.instance.viewWindow1.viewport.Project(v.Position, Camera.instance.Projection, Camera.instance.View, Matrix.Identity);
                    point.Z = 0;
                    g.device.DepthStencilState = DepthStencilState.None;
                    g.DrawRect(new System.Drawing.Rectangle((int)point.X - 5, (int)point.Y - 5, 10, 10), picked ? System.Drawing.Color.Red : System.Drawing.Color.Green);
                    g.device.DepthStencilState = DepthStencilState.Default;
                }
            }
        }
    }
}